[id].vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. <template>
  2. <div class="admin--sales-form">
  3. <div v-if="isLoading" class="admin--loading">
  4. 데이터를 불러오는 중...
  5. </div>
  6. <form v-else @submit.prevent="handleSubmit" class="admin--form">
  7. <!-- 전시장 -->
  8. <div class="admin--form-group">
  9. <label class="admin--form-label">전시장 <span class="admin--required">*</span></label>
  10. <select v-model.number="formData.showroom" class="admin--form-select" required>
  11. <option value="">전시장을 선택하세요</option>
  12. <option v-for="showroom in showrooms" :key="showroom.id" :value="showroom.id">
  13. {{ showroom.name }}
  14. </option>
  15. </select>
  16. </div>
  17. <!-- 영업팀 -->
  18. <div class="admin--form-group">
  19. <label class="admin--form-label">영업팀 <span class="admin--required">*</span></label>
  20. <select v-model.number="formData.team" class="admin--form-select" required>
  21. <option value="">영업팀을 선택하세요</option>
  22. <option v-for="team in teams" :key="team.id" :value="team.id">
  23. {{ team.name }}
  24. </option>
  25. </select>
  26. </div>
  27. <!-- 이름 -->
  28. <div class="admin--form-group">
  29. <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
  30. <input
  31. v-model="formData.name"
  32. type="text"
  33. class="admin--form-input"
  34. placeholder="이름을 입력하세요"
  35. required
  36. >
  37. </div>
  38. <!-- 직책 -->
  39. <div class="admin--form-group">
  40. <label class="admin--form-label">직책 <span class="admin--required">*</span></label>
  41. <select v-model.number="formData.position" class="admin--form-select" required>
  42. <option value="">직책을 선택하세요</option>
  43. <option :value="10">팀장</option>
  44. <option :value="15">마스터</option>
  45. <option :value="20">차장</option>
  46. <option :value="30">과장</option>
  47. <option :value="40">대리</option>
  48. <option :value="60">사원</option>
  49. </select>
  50. </div>
  51. <!-- 대표번호 -->
  52. <div class="admin--form-group">
  53. <label class="admin--form-label">대표번호 <span class="admin--required">*</span></label>
  54. <input
  55. v-model="formData.main_phone"
  56. type="tel"
  57. class="admin--form-input"
  58. placeholder="02-1234-5678"
  59. required
  60. >
  61. </div>
  62. <!-- 직통번호 -->
  63. <div class="admin--form-group">
  64. <label class="admin--form-label">직통번호</label>
  65. <input
  66. v-model="formData.direct_phone"
  67. type="tel"
  68. class="admin--form-input"
  69. placeholder="02-1234-5679"
  70. >
  71. </div>
  72. <!-- 핸드폰 -->
  73. <div class="admin--form-group">
  74. <label class="admin--form-label">핸드폰</label>
  75. <input
  76. v-model="formData.mobile"
  77. type="tel"
  78. class="admin--form-input"
  79. placeholder="010-1234-5678"
  80. >
  81. </div>
  82. <!-- 이메일 -->
  83. <div class="admin--form-group">
  84. <label class="admin--form-label">이메일</label>
  85. <input
  86. v-model="formData.email"
  87. type="email"
  88. class="admin--form-input"
  89. placeholder="email@example.com"
  90. >
  91. </div>
  92. <!-- 사진 -->
  93. <div class="admin--form-group">
  94. <label class="admin--form-label">사진</label>
  95. <input
  96. type="file"
  97. accept="image/*"
  98. class="admin--form-file"
  99. @change="handlePhotoUpload"
  100. >
  101. <div v-if="photoPreview || formData.photo_url" class="admin--image-preview">
  102. <img :src="photoPreview || getImageUrl(formData.photo_url)" alt="미리보기">
  103. <button type="button" class="admin--btn-remove-image" @click="removePhoto">
  104. 삭제
  105. </button>
  106. </div>
  107. </div>
  108. <!-- SACT -->
  109. <div class="admin--form-group">
  110. <label class="admin--form-label">SACT</label>
  111. <div class="admin--radio-group">
  112. <label class="admin--radio-label">
  113. <input v-model="formData.is_sact" type="radio" :value="true" name="is_sact">
  114. <span>예</span>
  115. </label>
  116. <label class="admin--radio-label">
  117. <input v-model="formData.is_sact" type="radio" :value="false" name="is_sact">
  118. <span>아니오</span>
  119. </label>
  120. </div>
  121. </div>
  122. <!-- TOP30 -->
  123. <div class="admin--form-group">
  124. <label class="admin--form-label">TOP30</label>
  125. <div class="admin--radio-group">
  126. <label class="admin--radio-label">
  127. <input v-model="formData.is_top30" type="radio" :value="true" name="is_top30">
  128. <span>예</span>
  129. </label>
  130. <label class="admin--radio-label">
  131. <input v-model="formData.is_top30" type="radio" :value="false" name="is_top30">
  132. <span>아니오</span>
  133. </label>
  134. </div>
  135. </div>
  136. <!-- 노출순서 -->
  137. <div class="admin--form-group">
  138. <label class="admin--form-label">노출순서</label>
  139. <input
  140. v-model.number="formData.display_order"
  141. type="number"
  142. class="admin--form-input"
  143. placeholder="숫자만 입력"
  144. min="0"
  145. >
  146. <p class="admin--form-help">숫자가 작을수록 먼저 노출됩니다.</p>
  147. </div>
  148. <!-- 버튼 영역 -->
  149. <div class="admin--form-actions">
  150. <button
  151. type="submit"
  152. class="admin--btn admin--btn-primary"
  153. :disabled="isSaving"
  154. >
  155. {{ isSaving ? '저장 중...' : '확인' }}
  156. </button>
  157. <button
  158. type="button"
  159. class="admin--btn admin--btn-secondary"
  160. @click="goToList"
  161. >
  162. 목록
  163. </button>
  164. </div>
  165. <!-- 성공/에러 메시지 -->
  166. <div v-if="successMessage" class="admin--alert admin--alert-success">
  167. {{ successMessage }}
  168. </div>
  169. <div v-if="errorMessage" class="admin--alert admin--alert-error">
  170. {{ errorMessage }}
  171. </div>
  172. </form>
  173. </div>
  174. </template>
  175. <script setup>
  176. import { ref, onMounted } from 'vue'
  177. import { useRoute, useRouter } from 'vue-router'
  178. definePageMeta({
  179. layout: 'admin',
  180. middleware: ['auth']
  181. })
  182. const route = useRoute()
  183. const router = useRouter()
  184. const { get, put, upload } = useApi()
  185. const { getImageUrl } = useImage()
  186. const isLoading = ref(true)
  187. const isSaving = ref(false)
  188. const successMessage = ref('')
  189. const errorMessage = ref('')
  190. const photoPreview = ref(null)
  191. const photoFile = ref(null)
  192. const showrooms = ref([])
  193. // 영업팀 수동 리스트 (0번째는 마스터팀)
  194. const teams = ref([
  195. { id: 0, name: '마스터팀' },
  196. { id: 1, name: '1팀' },
  197. { id: 2, name: '2팀' },
  198. { id: 3, name: '3팀' },
  199. { id: 4, name: '4팀' },
  200. { id: 5, name: '5팀' },
  201. { id: 6, name: '6팀' },
  202. { id: 7, name: '7팀' },
  203. { id: 8, name: '8팀' },
  204. { id: 9, name: '9팀' },
  205. { id: 10, name: '10팀' }
  206. ])
  207. const formData = ref({
  208. showroom: '',
  209. team: '',
  210. name: '',
  211. position: '',
  212. main_phone: '',
  213. direct_phone: '',
  214. mobile: '',
  215. email: '',
  216. photo_url: '',
  217. is_sact: false,
  218. is_top30: false,
  219. display_order: 0
  220. })
  221. // 필터 데이터 로드
  222. const loadFilters = async () => {
  223. // 전시장 리스트 (지점 목록)
  224. const { data: branchData, error: branchError } = await get('/branch/list', { per_page: 1000 })
  225. console.log('[SalesEdit] 전시장(지점) API 응답:', { data: branchData, error: branchError })
  226. if (branchData?.success && branchData?.data) {
  227. showrooms.value = branchData.data.items || []
  228. console.log('[SalesEdit] 전시장(지점) 로드 성공')
  229. }
  230. }
  231. // 데이터 로드
  232. const loadSales = async () => {
  233. isLoading.value = true
  234. const id = route.params.id
  235. const { data, error } = await get(`/staff/sales/${id}`)
  236. console.log('[SalesEdit] 데이터 로드:', { data, error })
  237. if (data?.success && data?.data) {
  238. const sales = data.data
  239. formData.value = {
  240. showroom: sales.showroom || '',
  241. team: sales.team || '',
  242. name: sales.name || '',
  243. position: sales.position || '',
  244. main_phone: sales.main_phone || '',
  245. direct_phone: sales.direct_phone || '',
  246. mobile: sales.mobile || '',
  247. email: sales.email || '',
  248. photo_url: sales.photo_url || '',
  249. is_sact: sales.is_sact || false,
  250. is_top30: sales.is_top30 || false,
  251. display_order: sales.display_order || 0
  252. }
  253. photoPreview.value = sales.photo_url || null
  254. console.log('[SalesEdit] 로드 성공')
  255. }
  256. isLoading.value = false
  257. }
  258. // 사진 업로드
  259. const handlePhotoUpload = (event) => {
  260. const file = event.target.files[0]
  261. if (!file) return
  262. if (!file.type.startsWith('image/')) {
  263. alert('이미지 파일만 업로드 가능합니다.')
  264. return
  265. }
  266. photoFile.value = file
  267. const reader = new FileReader()
  268. reader.onload = (e) => {
  269. photoPreview.value = e.target.result
  270. }
  271. reader.readAsDataURL(file)
  272. }
  273. // 사진 삭제
  274. const removePhoto = () => {
  275. photoPreview.value = null
  276. photoFile.value = null
  277. formData.value.photo_url = ''
  278. }
  279. // 폼 제출
  280. const handleSubmit = async () => {
  281. successMessage.value = ''
  282. errorMessage.value = ''
  283. if (formData.value.showroom === '' || formData.value.showroom === null) {
  284. errorMessage.value = '전시장을 선택하세요.'
  285. return
  286. }
  287. if (formData.value.team === '' || formData.value.team === null) {
  288. errorMessage.value = '영업팀을 선택하세요.'
  289. return
  290. }
  291. if (!formData.value.name) {
  292. errorMessage.value = '이름을 입력하세요.'
  293. return
  294. }
  295. if (formData.value.position === '' || formData.value.position === null) {
  296. errorMessage.value = '직책을 선택하세요.'
  297. return
  298. }
  299. isSaving.value = true
  300. try {
  301. let photoUrl = formData.value.photo_url
  302. // 새 사진 업로드
  303. if (photoFile.value) {
  304. const formDataImage = new FormData()
  305. formDataImage.append('file', photoFile.value)
  306. const { data: uploadData, error: uploadError } = await upload('/upload/staff-image', formDataImage)
  307. console.log('[SalesEdit] 이미지 업로드 응답:', { data: uploadData, error: uploadError })
  308. if (uploadError) {
  309. errorMessage.value = '사진 업로드에 실패했습니다: ' + (uploadError.message || uploadError)
  310. isSaving.value = false
  311. return
  312. }
  313. if (!uploadData?.success || !uploadData?.data?.url) {
  314. errorMessage.value = '사진 업로드 응답이 올바르지 않습니다.'
  315. isSaving.value = false
  316. return
  317. }
  318. photoUrl = uploadData.data.url
  319. console.log('[SalesEdit] 업로드된 이미지 URL:', photoUrl)
  320. }
  321. const submitData = {
  322. ...formData.value,
  323. photo_url: photoUrl
  324. }
  325. const id = route.params.id
  326. const { data, error } = await put(`/staff/sales/${id}`, submitData)
  327. if (error) {
  328. errorMessage.value = error.message || '수정에 실패했습니다.'
  329. } else {
  330. successMessage.value = '영업사원 정보가 수정되었습니다.'
  331. setTimeout(() => {
  332. router.push('/admin/staff/sales')
  333. }, 1000)
  334. }
  335. } catch (error) {
  336. errorMessage.value = '서버 오류가 발생했습니다.'
  337. console.error('Save error:', error)
  338. } finally {
  339. isSaving.value = false
  340. }
  341. }
  342. const goToList = () => {
  343. router.push('/admin/staff/sales')
  344. }
  345. onMounted(async () => {
  346. await loadFilters()
  347. await loadSales()
  348. })
  349. </script>